package visitor;

import java.util.*;
import syntaxtree.*;
import symbol.*;

public class TypeChecking implements TypeVisitor
{

	private ClassTable currentClass;
	private MethodTable currentMethod;
	private ProgramTable pt;
	private ErrorMsg error;
	
	public boolean hasError()
	{
		return error.hasError();
	}
	public TypeChecking (ProgramTable pt)
	{
		this.pt = pt;
	}
	
	// MainClass m;
	// ClassDeclList cl;
	public Type visit(Program n) {
		error = new ErrorMsg();
		n.m.accept(this);
		for ( int i = 0; i < n.cl.size(); i++ ) {
			n.cl.elementAt(i).accept(this);
		}
		return null;
	}

	// Identifier i1,i2;
	// Statement s;
	public Type visit(MainClass n) {
		n.i1.accept(this);
		currentClass = pt.getClass(Symbol.symbol(n.i1.toString()));
		n.i2.accept(this);
		currentMethod = currentClass.getMethod(Symbol.symbol(n.i2.toString()));
		n.s.accept(this);
		return null;
	}

	// Identifier i;
	// VarDeclList vl;
	// MethodDeclList ml;
	public Type visit(ClassDeclSimple n) {
		n.i.accept(this);
		currentClass = pt.getClass(Symbol.symbol(n.i.toString()));
		currentMethod = null;
		for ( int i = 0; i < n.vl.size(); i++ ) {
			n.vl.elementAt(i).accept(this);
		}
		for ( int i = 0; i < n.ml.size(); i++ ) {
			n.ml.elementAt(i).accept(this);
		}
		return null;
	}

	// Identifier i;
	// Identifier j;
	// VarDeclList vl;
	// MethodDeclList ml;
	public Type visit(ClassDeclExtends n) {
		n.i.accept(this);
		currentClass = pt.getClass(Symbol.symbol(n.i.toString()));
		n.j.accept(this);
		currentMethod = null;
		for ( int i = 0; i < n.vl.size(); i++ ) {
			n.vl.elementAt(i).accept(this);
		}
		for ( int i = 0; i < n.ml.size(); i++ ) {
			n.ml.elementAt(i).accept(this);
		}
		return null;
	}

	// Type t;
	// Identifier i;
	public Type visit(VarDecl n) {
		n.t.accept(this);
		n.i.accept(this);
		return null;
	}

	// Type t;
	// Identifier i;
	// FormalList fl;
	// VarDeclList vl;
	// StatementList sl;
	// Exp e;
	public Type visit(MethodDecl n) {
		n.t.accept(this);
		n.i.accept(this);
		currentMethod = currentClass.getMethod(Symbol.symbol(n.i.toString()));
		for ( int i = 0; i < n.fl.size(); i++ ) {
			n.fl.elementAt(i).accept(this);
		}
		for ( int i = 0; i < n.vl.size(); i++ ) {
			n.vl.elementAt(i).accept(this);
		}
		for ( int i = 0; i < n.sl.size(); i++ ) {
			n.sl.elementAt(i).accept(this);
		}
		Type t = n.e.accept(this);
		if (!t.toString().equals(n.t.toString()))
			error.complain("Return expression does not match with the return type of method "+currentMethod.toString());
		return null;
	}

	// Type t;
	// Identifier i;
	public Type visit(Formal n) {
		n.t.accept(this);
		n.i.accept(this);
		return null;
	}

	public Type visit(IntArrayType n) {
		return new IntArrayType();
	}

	public Type visit(BooleanType n) {
		return new BooleanType();
	}

	public Type visit(IntegerType n) {
		return new IntegerType();
	}

	// String s;
	public Type visit(IdentifierType n) {
		return n;
	}

	// StatementList sl;
	public Type visit(Block n) {
		for ( int i = 0; i < n.sl.size(); i++ ) {
			n.sl.elementAt(i).accept(this);
		}
		return null;
	}

	// Exp e;
	// Statement s1,s2;
	public Type visit(If n) {
		if (! (n.e.accept(this) instanceof BooleanType))
			error.complain("If statement condition must be boolean");
		n.s1.accept(this);
		n.s2.accept(this);
		return null;
	}

	// Exp e;
	// Statement s;
	public Type visit(While n) {
		if (! (n.e.accept(this) instanceof BooleanType))
			error.complain("While statement condition must be boolean");
		n.s.accept(this);
		return null;
	}

	// Exp e;
	public Type visit(Print n) {
		n.e.accept(this);
		return null;
	}

	// Identifier i;
	// Exp e;
	public Type visit(Assign n) {
		Type var = whatType(n.i.toString());
		Type exp = n.e.accept(this);
		if (var instanceof IdentifierType) //Class
		{
			ClassTable c = pt.getClass(Symbol.symbol(((IdentifierType) var).toString()));
			if (c == null || !c.toString().equals(exp.toString()))
				error.complain("Assign types not maching");
		}
		else
		{
			if (var == null || exp == null || !var.toString().equals(exp.toString()))
				error.complain("Assign types not maching");
		}
		return null;
	}

	// Identifier i;
	// Exp e1,e2;
	public Type visit(ArrayAssign n) {
		Type var = whatType(n.i.toString());
		if (var == null)
			error.complain("Array Type not declared");
		else if (!(var instanceof IntegerType))
			error.complain("Array must be integer");
		if (! (n.e1.accept(this) instanceof IntegerType))
			error.complain("Iterator of Array must be integer");
		if (! (n.e2.accept(this) instanceof IntegerType))
			error.complain("Right side expression of Array Assign must be integer");
		return null;
	}

	// Exp e1,e2;
	public Type visit(And n) {
		if (! (n.e1.accept(this) instanceof BooleanType))
			error.complain("Left side of operator && must be boolean");
		if (! (n.e2.accept(this) instanceof BooleanType))
			error.complain("Right side of operator && must be boolean");
		return new BooleanType();
	}

	// Exp e1,e2;
	public Type visit(LessThan n) {
		if (! (n.e1.accept(this) instanceof IntegerType))
			error.complain("Left side of operator < must be integer");
		if (! (n.e2.accept(this) instanceof IntegerType))
			error.complain("Right side of operator < must be integer");
		return new BooleanType();
	}

	// Exp e1,e2;
	public Type visit(Plus n) {
		if (! (n.e1.accept(this) instanceof IntegerType))
			error.complain("Left side of operator + must be integer");
		if (! (n.e2.accept(this) instanceof IntegerType))
			error.complain("Right side of operator + must be integer");
		return new IntegerType();
	}

	// Exp e1,e2;
	public Type visit(Minus n) {
		if (! (n.e1.accept(this) instanceof IntegerType))
			error.complain("Left side of operator - must be integer");
		if (! (n.e2.accept(this) instanceof IntegerType))
			error.complain("Right side of operator - must be integer");
		return new IntegerType();
	}

	// Exp e1,e2;
	public Type visit(Times n) {
		if (! (n.e1.accept(this) instanceof IntegerType))
			error.complain("Left side of operator * must be integer");
		if (! (n.e2.accept(this) instanceof IntegerType))
			error.complain("Right side of operator * must be integer");
		return new IntegerType();
	}

	// Exp e1,e2;
	public Type visit(ArrayLookup n) {
		if (! (n.e1.accept(this) instanceof IntegerType))
			error.complain("Array must be integer");
		if (! (n.e2.accept(this) instanceof IntegerType))
			error.complain("Iterator of Array must be integer");
		return new IntegerType();
	}

	// Exp e;
	public Type visit(ArrayLength n) {
		if (! (n.e.accept(this) instanceof IntegerType))
			error.complain("Array Length must be integer");
		return null;
	}

	// Exp e;
	// Identifier i;
	// ExpList el;
	public Type visit(Call n) {
		Type t = n.e.accept(this);
		if (t == null || !(t instanceof IdentifierType))
			error.complain("Calling some unknown class");
		ClassTable c = pt.getClass(Symbol.symbol(((IdentifierType) t).toString()));
		MethodTable m = c.getMethod(Symbol.symbol(n.i.toString()));
		if (m == null)
		{
			error.complain("Calling some unknown method");
			return null;
		}
		if (n.el.size() != m.getNumParam())
		{
			error.complain("Incorrect number of parameters for "+m.toString()+": Expected "+m.getNumParam()+" founded "+n.el.size());
			return null;
		}
		for ( int i = 0; i < n.el.size(); i++ ) {
			Type tparam = n.el.elementAt(i).accept(this);
			Type ti = m.getParam(i+1);
			if (tparam instanceof IdentifierType) //Class
			{
				ClassTable cp = pt.getClass(Symbol.symbol(((IdentifierType) tparam).toString()));
				if (cp == null || !cp.toString().equals(ti.toString()))
					error.complain("Argument "+(i+1)+" of method "+m.toString()+"must be "+ti.toString());
			}
			else
			{
				if ((tparam == null) || !tparam.toString().equals(ti.toString()))
					error.complain("Argument "+(i+1)+" of method "+m.toString()+"must be "+ti.toString());
			}
		}
		return m.getType();
	}

	// int i;
	public Type visit(IntegerLiteral n) {
		return new IntegerType();
	}

	public Type visit(True n) {
		return new BooleanType();
	}

	public Type visit(False n) {
		return new BooleanType();
	}

	// String s;
	public Type visit(IdentifierExp n) {
		Type t = whatType(n.s);
		if (t == null)
			error.complain("Identifier not found");
		return t;
	}

	public Type visit(This n) {
		if (currentClass == null)
			error.complain("Class Environment not found");
		return new IdentifierType(currentClass.toString());
	}

	// Exp e;
	public Type visit(NewArray n) {
		if (! (n.e.accept(this) instanceof IntegerType))
			error.complain("Array size must be integer");
		return new IntArrayType();
	}

	// Identifier i;
	public Type visit(NewObject n) {
		ClassTable c = pt.getClass(Symbol.symbol(n.i.toString()));
		if (c == null)
			error.complain("Class used in new object not found");
		return new IdentifierType(c.toString());
	}

	// Exp e;
	public Type visit(Not n) {
		n.e.accept(this);
		return new BooleanType();
	}

	// String s;
	public Type visit(Identifier n) {
		return whatType(n.s);
	}
	
	public void PrettyPrint()
	{
		pt.PrettyPrint();
	}
	public Type whatType(String id)
	{
		if ((currentMethod != null) && (currentMethod.getVar(Symbol.symbol(id)) != null))
			return currentMethod.getVar(Symbol.symbol(id));
		if ((currentMethod != null) && (currentMethod.getParam(Symbol.symbol(id)) != null))
			return currentMethod.getParam(Symbol.symbol(id));
		if ((currentClass != null) && (currentClass.getVar(Symbol.symbol(id)) != null))
			return currentClass.getVar(Symbol.symbol(id));
		if (pt.getClass(Symbol.symbol(id)) != null)
			return new IdentifierType(id);
		return null;
	}
}
